iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0
自我挑戰組

模仿知名網站的外觀系列 第 26

【Day26】模仿知名網站的外觀 X(13) 完成追隨和喜歡功能

  • 分享至 

  • xImage
  •  

上一回,我們完成了點擊進入貼文區塊的功能,這一次我們要開發追隨和喜歡功能。

在api資料夾,新增follow.ts,提供追隨和取消追隨的API,對資料庫中的用戶追蹤清單進行修改。

import { NextApiRequest, NextApiResponse } from "next";
import serverAuth from "@/libs/serverAuth";
import prisma from "@/libs/prismadb";

export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse
) {
	if (req.method !== "POST" && req.method !== "DELETE") {
		return res.status(405).end();
	}

	try {
		const { userId } = req.body;
		const { currentUser } = await serverAuth(req, res);

		if (!userId || typeof userId !== "string") {
			throw new Error("Invalid ID");
		}

		const user = await prisma.user.findUnique({
			where: {
				id: userId,
			},
		});

		if (!user) {
			throw new Error("Invalid ID");
		}

		let updatedFollowingIds = { ...(user.followingIds || []) };

		if (req.method === "POST") {
			updatedFollowingIds.push(userId);
		}

		if (req.method === "DELETE") {
			updatedFollowingIds = updatedFollowingIds.filter(
				(followingId) => followingId !== userId
			);
		}

		const updateUser = await prisma.user.update({
			where: {
				id: currentUser.id,
			},
			data: {
				followingIds: updatedFollowingIds,
			},
		});

		return res.status(200).json(updateUser);
	} catch (error) {
		console.log(error);
		return res.status(400).end();
	}
}

在Hooks資料夾下,建立useFollow.ts,透過API實現追蹤和取消追蹤用戶。

import { useCallback, useMemo } from "react";
import useCurrentUser from "./useCurrentUser";
import useLoginModal from "./useLoginModal";
import useUser from "./useUser";
import axios from "axios";
import toast from "react-hot-toast";

const useFollow = (userId: string) => {
	const { data: currentUser, mutate: mutateCurrentUser } = useCurrentUser();
	const { mutate: mutateFetchedUser } = useUser(userId);

	const loginModal = useLoginModal();

	const isFollowing = useMemo(() => {
		const list = currentUser?.followingIds || [];
		return list.includes(userId);
	}, [userId, currentUser?.followingIds]);

	const toggleFollow = useCallback(async () => {
		if (!currentUser) {
			return loginModal.onOpen();
		}

		try {
            let request;

            if(isFollowing){
                request = () => axios.delete("/api/follow", {data: {userId}});
            }
            else{
                request = () => axios.post("/api/follow", {userId});
            }

            await request();

            mutateCurrentUser();
            mutateFetchedUser();

            toast.success("Success");
		} catch (error) {
			toast.error("Something went wrong");
		}
	}, [currentUser, isFollowing, userId, mutateCurrentUser, mutateFetchedUser, loginModal]);

    return { isFollowing, toggleFollow };
};

export default useFollow;

修改UserBio.tsx,在個人檔案頁面按下寫著Follow的按鈕可以進行追蹤,按下Unfollow可取消追蹤。

import { useMemo } from "react";
import { format } from "date-fns";
import useCurrentUser from "@/Hooks/useCurrentUser";
import useUser from "@/Hooks/useUser";
import Button from "../Button";
import { BiCalendar } from "react-icons/bi";
import useEditModal from "@/Hooks/useEditModal";
import useFollow from "@/Hooks/useFollow";

interface UserBioProps {
    userId: string;
}

const UserBio: React.FC<UserBioProps> = ({ userId }) => {
    const {data: currentUser} = useCurrentUser();
    const {data: fetchedUser} = useUser(userId);

    const editModal = useEditModal();

    const {isFollowing, toggleFollow} = useFollow(userId);

    const createdAt = useMemo(() => {
        if(!fetchedUser?.createdAt){
            return null;
        }

        return format(new Date(fetchedUser.createdAt), 'MMMM yyyy');
    }, [fetchedUser?.createdAt]);

  return (
    <div className="border-b-[1px] border-neutral-800 pb-4">
        <div className="flex justify-end p-2">
            {
                currentUser?.id === userId ? (
                    <Button secondary label="Edit" onClick={editModal.onOpen} />
                ) : (
                <Button secondary={!isFollowing}
                label={isFollowing ? "Unfollow" : "Follow"}
                onClick={toggleFollow}
                outline={isFollowing}
                />
                )
            }
        </div>
        <div className="mt-8 px-4">
            <div className="flex flex-col">
                <p className="text-white text-2xl font-semibold">
                {fetchedUser?.name}
                </p>
                <p className="text-md text-neutral-500">
                @{fetchedUser?.username}
                </p>
            </div>
            <div className="flex flex-col mt-4">
                <p className="text-white">
                    {fetchedUser?.bio}
                </p>
                <div className="flex flex-row items-center gap-2 mt-4 text-neutral-500">
                    <BiCalendar size={24}/>
                    <p>
                        Joined {createdAt}
                    </p>
                </div>
            </div>
            <div className="flex flex-row items-center mt-4 gap-6">
                <div className="flex flex-row items-center gap-1">
                    <p className="text-white">
                        {fetchedUser?.followingIds?.length}
                    </p>
                    <p className="text-neutral-500">
                        Following
                    </p>
                </div>
                <div className="flex flex-row items-center gap-1">
                    <p className="text-white">
                        {fetchedUser?.followersCount || 0}
                    </p>
                    <p className="text-neutral-500">
                        Followers
                    </p>
                </div>
            </div>
        </div>
    </div>
  )
}

export default UserBio

接下來要編寫貼文的評論區塊。

在api/posts下,建立[postId].ts,從資料庫裡面取得符合與postId相同的內容後,回傳其中的user和 comments部分。

import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@/libs/prismadb";

export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse
) {
	if (req.method !== "GET") {
		return res.status(405).end();
	}

	try {
		const { postId } = req.query;

		if (!postId || typeof postId !== "string") {
			throw new Error("Invalid ID");
		}

		const post = await prisma.post.findUnique({
			where: {
				id: postId,
			},
			include: {
				user: true,
				comments: {
					include: {
						user: true,
					},
					orderBy: {
						createdAt: "desc",
					},
				},
			},
		});

		return res.status(200).json(post);
	} catch (error) {
		console.log(error);
		return res.status(400).end();
	}
}

在Hooks下,新增usePost.ts,透過API取得貼文內容。

import useSWR, { mutate } from "swr";
import fetcher from "@/libs/fetcher";

const usePost = (postId?: string) => {
    const url = postId ? `/api/posts/${postId}` : null
const {data, error, isLoading, mutate} = useSWR(url, fetcher)

  return {
    data,
    error,
    isLoading,
    mutate
  }
}

export default usePost

在pages下,建立posts資料夾,在posts資料夾下,新增[postId].tsx,顯示特定貼文的內容和輸入評論的區塊。

import { useRouter } from "next/router";
import { ClipLoader } from "react-spinners";
import usePost from "@/Hooks/usePost";
import Header from "@/Components/layout/Header";
import PostItem from "@/Components/posts/PostItem";
import Form from "@/Components/Form";

const PostView = () => {
    const router = useRouter();
    const { postId } = router.query;

    const { data: fetchedPost, isLoading } = usePost(postId as string);

    if(isLoading || !fetchedPost) {
        return (
            <div className="flex justify-center items-center h-full">
                <ClipLoader />
            </div>
        )
    }

    return (
        <>
            <Header label="Post" showBackArrow />
            <PostItem data={fetchedPost} />
            <Form postId={postId as string} isComment placeholder="Post your reply"/>
        </>
    )
}

export default PostView

然後我們要實現貼文的喜歡功能。

在pages/api資料夾下,新增like.ts,提供API,當用戶按下喜歡或是取消喜歡時對資料庫中的內容修改。

import { NextApiRequest, NextApiResponse } from "next"; 
import serverAuth from "@/libs/serverAuth";
import prisma from "@/libs/prismadb";

export default async function handler(
    req: NextApiRequest,
    res: NextApiResponse
    ){
    if(req.method !== 'POST' && req.method !== 'DELETE'){
        return res.status(405).end();
    }

    try{
        const { postId } = req.body;
        const { currentUser } = await serverAuth(req, res);

        if(!postId || typeof postId !== 'string'){
            throw new Error('Invalid ID');
        }

        const post = await prisma.post.findUnique({
            where: {
                id: postId
            }
        });

        if(!post){
            throw new Error('Invalid ID');
        }

        let updatedLikedIds = [ ...(post.likedIds || []) ];

        if(req.method === 'POST'){
            updatedLikedIds.push(currentUser.id);
        }
        if(req.method === 'DELETE'){
            updatedLikedIds = updatedLikedIds.filter((likedId) => likedId !== currentUser.id);
        }

        const updatedPost = await prisma.post.update({
            where: {
                id: postId
            },
            data: {
                likedIds: updatedLikedIds
            }
        });

        return res.status(200).json(updatedPost);

    } catch(error){
        console.log(error);
        return res.status(400).end();
    }
}

在Hooks資料夾下,新增useLIke.ts,使用API實現用戶喜歡和取消喜歡的功能。

import { useCallback, useMemo } from "react";
import useCurrentUser from "./useCurrentUser";
import useLoginModal from "./useLoginModal";
import usePost from "./usePost";
import usePosts from "./usePosts";
import toast from "react-hot-toast";
import axios from "axios";

const useLike = ({ postId, userId }: { postId: string; userId?: string }) => {
	const { data: currentUser } = useCurrentUser();
	const { data: fetchedPost, mutate: mutateFetchedPost } = usePost(postId);
	const { mutate: mutateFetchedPosts } = usePosts(userId);

    const loginModal = useLoginModal();

    const hasLiked = useMemo(() => {
        const list = fetchedPost?.likedIds || [];
        return list.includes(currentUser?.id);
    }, [currentUser?.id, fetchedPost?.likedIds]);

    const toggleLike = useCallback(async () => {
        if(!currentUser){
            return loginModal.onOpen();
        }

        try{
            let request;

            if(hasLiked){
                request = () => axios.delete("/api/like", {data: {postId}});
            }
            else{
                request = () => axios.post("/api/like", {postId});
            }

            await request();
            mutateFetchedPost();
            mutateFetchedPosts();
            
            toast.success("Success");
        } catch(error){
            toast.error("Something went wrong");
        }
    }, [currentUser, hasLiked, postId, mutateFetchedPost, mutateFetchedPosts, loginModal]);
    
    return { hasLiked, toggleLike };
};

export default useLike;

修改PostItem.tsx,完成用戶喜歡和取消喜歡功能。

import { useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import useLoginModal from "@/Hooks/useLoginModal";
import useCurrentUser from "@/Hooks/useCurrentUser";
import { formatDistanceToNowStrict } from "date-fns";
import Avatar from "../Avatar";
import { AiOutlineHeart, AiFillHeart, AiOutlineMessage } from "react-icons/ai";
import useLike from "@/Hooks/useLike";

interface PostItemProps {
	data: Record<string, any>;
	userId?: string;
}

const PostItem: React.FC<PostItemProps> = ({ data, userId }) => {
	const router = useRouter();
	const loginModal = useLoginModal();

	const { data: currentUser } = useCurrentUser();
    const { hasLiked, toggleLike } = useLike({ postId: data.id, userId });

	const goToUser = useCallback(
		(event: any) => {
			event.stopPropagation();

			router.push(`/users/${data.user.id}`);
		},
		[router, data.user.id]
	);

	const goToPost = useCallback(() => {
		router.push(`/posts/${data.id}`);
	}, [router, data.id]);

    const onLike = useCallback((event: any) => {
        event.stopPropagation();

        if(!currentUser){
            return loginModal.onOpen();
        }

        toggleLike();
    }, [loginModal, currentUser, toggleLike]);

    const createdAt = useMemo(() => {
        if(!data?.createdAt) {
            return null;
        }
        return formatDistanceToNowStrict(new Date(data.createdAt));
    }, [data?.createdAt]);

    const LikeIcon = hasLiked ? AiFillHeart : AiOutlineHeart;

	return (
        <div onClick={goToPost} className="border-b-[1px] border-neutral-800 p-5 cursor-pointer hover:bg-neutral-900 transition">
            <div className="flex flex-row items-start gap-3">
                <Avatar userId={data.user.id}/>
                <div>
                    <div className="flex flex-row items-center gap-2">
                        <p onClick={goToUser} className="text-white font-semibold cursor-pointer hover:underline">{data.user.name}</p>
                        <span onClick={goToUser} className="text-neutral-500 cursor-pointer hover:underline hidden md:block">@{data.user.username}</span>
                        <span className="text-neutral-500 text-sm">{createdAt}</span>
                    </div>
                    <div className="text-white mt-1">
                        {data.content}
                    </div>
                    <div className="flex flex-row items-center mt-3 gap-10">
                        <div className="flex flex-row items-center text-neutral-500 gap-2 cursor-pointer transition hover:text-sky-500">
                            <AiOutlineMessage size={20} />
                            <p>
                                {data.comments?.length || 0}
                            </p>
                        </div>
                        <div onClick={onLike} className="flex flex-row items-center text-neutral-500 gap-2 cursor-pointer transition hover:text-red-500">
                            <LikeIcon size={20} color={hasLiked ? "red" : ""}/>
                            <p>
                                {data.likedIds?.length}
                            </p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
};

export default PostItem;

啟動專案,進入貼文按下喜歡,可以看到喜歡數增加了。

Untitled

我們完成了喜歡的功能,但是評論功能還沒做出來,下一回我們會完成評論功能。


上一篇
【Day25】模仿知名網站的外觀 X(12) 完成首頁貼文區塊
下一篇
【Day27】模仿知名網站的外觀 X(14) 評論和通知
系列文
模仿知名網站的外觀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言